⚠️ You are offline — changes will sync when reconnected.
Silstate
Savings Group Management · Silstate Business Solutions Ltd.
Select Your Group
Choose which savings group to sign into
🏦
Loading groups…

Powered by Silstate Business Solutions Ltd.
SILSTATE
S
Side Savers
Village Banking System · Aug 2025 – Jul 2026
💡 Loading quote…

Welcome

Side Savers Group · Aug 2025 – Jul 2026

Total Members
10
Active
Total Savings
K 289,500
Cumulative
Outstanding Loans
K 301,918
Receivable
Interest Earned
K 109,518
Compound (year)
Next Month Interest
K 0
10% / month on all loans
Pending Approvals
0
Awaiting treasurer
Member Savings Overview Current
MemberSavingsOutstandingNext Int. Due (10%/mo)Status
Monthly Savings Trend
Recent Receipts

Executive & Committee

Group leadership and member profiles

Executive Committee
All Member Profiles

Members Directory

Performance summary – all members

Performance Summary (Aug 2025 – Jul 2026)
#MemberRoleSavedEligibilityLoans TakenRepaidOutstandingNext Int. (10%/mo)Net Pos.

Interest Schedule

Monthly interest payable at 10% per month on outstanding loan balances

💹 Interest is charged at 10% per month (flat rate) on each member's outstanding loan balance. This shows what is due next month.
Next Month Interest Payable – All Members
#MemberRoleOutstanding Loan (ZMW)RateInterest Due Next MonthInterest (3 Months)Interest (12 Months)
Interest Projection – 12 Month Rolling View (assumes no repayments – interest on static balance at 10%/month)
Member

Group Reports

Annual financials – visible to all members

Total Income
K 609,358
Annual
Contributions
K 310,500
Savings + fees
Loan Interest
K 92,483
Received
Loans Disbursed
K 537,935
Aug–Mar 2026
Monthly Performance
MonthSavingsInterestCumulativeDisbursedOutstanding
Aug 2532,500032,50032,50032,500
Sep 2523,0003,25055,50044,42862,428
Oct 2527,7505,55083,25057,00099,262
Nov 2537,7508,325121,00067,800144,980
Dec 2537,00012,100158,00072,500197,750
Jan 2628,00015,800186,00069,030248,465
Feb 2683,50018,600269,500169,677358,661
Mar 2620,00026,950289,50025,000301,918
Apr 26028,950289,5000301,918
May 26028,950289,5000301,918
Jun 26028,950289,5000301,918
Jul 26028,950289,5000301,918
TOTAL289,500206,375537,935
Balance Sheet – Jul 31, 2026
Assets
Bank BalanceK 1,116
Member SavingsK 289,500
Compound InterestK 109,518
Membership FeesK 2,500
Birthday FundK 11,050
Social FundK 7,450
Total Current AssetsK 420,018
Outstanding LoansK 301,918
TOTAL ASSETSK 721,935
Members' Equity
Opening EquityK 37,000
TOTAL EQUITYK 721,935
Annual P&L
Line ItemAugSepOctNovDecJanFebMarApr 26May 26Jun 26Jul 26Total
▶ INCOME
Member Savings32,50023,00027,75037,75037,00028,00083,50020,0000000289,500
Membership Fees2,0002502500000000002,500
Birthday Fund1,2001,3501,5001,5001,5001,5001,5001,000000011,050
Social Fund8009001,0001,0001,0001,0001,00075000007,450
Loan Interest006,2436,24314,49819,77520,69725,028000092,483
Compound Interest on Savings03,2505,5508,32512,10015,80018,60026,95028,95028,95028,95028,950206,375
TOTAL INCOME36,50028,75042,29354,81866,09866,075125,29773,72828,95028,95028,95028,950609,358
▶ OUTFLOWS
Loans Disbursed32,50044,42857,00067,80072,50069,030169,67725,0000000537,935

Transactions

Submit deposits and loan repayments for treasurer verification

💰 Deposit / Contributions
🔄 Loan Repayment
New Deposit Entry

Standard Monthly Guidelines
Contributions
• Membership Fee: K250 (first month only)
• Birthday Contribution: K150
• Social Fund: K100
• Savings: Variable (min K500)

Process
1. Complete & submit this form
2. Treasurer verifies and approves
3. Official receipt issued to you
4. Recorded in your savings statement
Deposit Preview
Fill form to preview

Disburse Loan

Treasurer: post and approve a new loan to a member

🔐 Only the Treasurer can disburse loans. A receipt and updated loan statement are generated immediately.
New Loan Disbursement

Eligibility Checker
Select a member to check eligibility.

Rules:
• Maximum = 4× cumulative savings
• Rate: 10% per month (flat rate)
• Interest charged on outstanding balance
• Must be in good standing
Repayment Schedule
Enter loan details

Savings Statement

Monthly savings and interest history

📊
Select a member

Loan Statement

Complete loan history with interest schedule

📄
Select a member

Post Monthly Interest

Treasurer: record savings interest earned for each member — adds to their cumulative total

🔐 Only the Treasurer can post interest. Each posting adds to the member's monthly interest and total interest earned in the savings statement.
Post Interest Entry

Enter manually or use the auto-suggested amount above

All Members – Interest Due Jul 26
Member Savings Already Posted Auto-Suggest Action
Interest Posting History
No interest postings yet

Group Expenses

Treasurer: record all group expenditure with receipt attachments

🔐 Treasurer only. Expenses reduce the group's cash position and appear in the P&L report.
Post New Expense
📎 Click to attach receipt (image or PDF, max 2MB)
Summary
By Category
Expense Ledger
🧾
No expenses recorded yet

Bank Deposits

Treasurer: record monthly deposits taken to the bank — auto-aggregated from member contributions

🏦 After collecting from all members each month, record the total amount banked here and attach the bank slip.
Record Monthly Bank Deposit
💡 Auto-Calculated from Member Deposits for This Month
Select a month above to see breakdown
Breakdown (edit if needed):
📎 Click to attach bank deposit slip
Monthly Bank Reconciliation
MonthTotal BankedSavingsOther FundsBank RefDateSlip
No bank deposits yet
⚠️ Months Not Yet Banked
Loading…
Annual Bank Summary
No data yet

Compound Interest

Treasurer: accrue savings interest on cumulative member balances

Post Compound Interest Entry
Member Savings Details
Select a member to load details.
Calculated Interest
Fill in details above
All Members — Jul 26
MemberCum. SavingsAlready PostedCalculatedAction
Compound Interest History
No entries yet

Reverse Transactions

Treasurer: reverse incorrect transactions — the affected member is notified automatically

⚠️ Reversals adjust balances immediately, create an audit trail, and send the member an in-app notification with your reason.
Select Transaction to Reverse
↩️
No transactions found
Reversal Audit Trail
No reversals yet

Notifications

Messages and alerts about your account

🔔
No notifications yet

Verify & Approve

Treasurer: review and approve submitted entries

🔐 Treasurer mode. Approvals generate official receipts and update balances.
Deposits 0
Repayments 0
💰
No pending deposits

Edit Entries

Treasurer: edit any member record or transaction

⚠️ Changes take effect immediately and update all statements.
Members
Balances
Transaction Log
Member Directory
NameRolePhoneStatusActions

Official Receipt

Treasurer-verified transaction

Group Settings

Manage branding, group info and PINs

Group Logo
S

Click to upload logo

PNG or JPG, square preferred

Group Information
🧮
Interest & Penalty Formula Editor
All changes are logged. Live preview updates instantly.
Treasurer Only
⚠️ Changes to these formulas affect all future interest calculations, loan disbursement schedules and repayment amounts. Existing approved receipts are not retroactively changed.
📋 Loan Interest
Formula: Interest = Balance × 10%
💰 Savings Interest
Formula: Interest = Balance × 0.4167%/month (compound)
🏦 Loan Eligibility
Max Loan = Savings × 4
⚠️ Penalty & Late Fees
No penalty currently applied
🔬 Live Formula Tester — enter any amount to preview
Enter amounts above to see the formula in action.
Member PINs
MemberNew PINAction

My Profile

View your account details and change your PIN

Account Information
AP
Member
Occupation
Phone
Status
Cumulative Savings
Outstanding Loan
Next Month Interest (10%/month)
Loan Eligibility
Interest Earned

Change My PIN
🔒 Your PIN keeps your account secure. Choose something memorable but not obvious. Minimum 4 characters.
Update My Contact Details
`); } else { w.document.write(` `); } } // ═══════════════════════════════════════════════════════════ // BANK DEPOSITS // ═══════════════════════════════════════════════════════════ window._bdSlip=null; function handleBDSlip(e){ const file=e.target.files[0]; if(!file) return; if(file.size>2*1024*1024){alert('File too large.');return;} const r=new FileReader(); r.onload=ev=>{ window._bdSlip=ev.target.result; const isImg=file.type.startsWith('image/'); document.getElementById('bd-file-preview').innerHTML= isImg?`
✅ ${file.name}
` :`
📄 ${file.name} attached
`; }; r.readAsDataURL(file); } function loadBDMonth(){ const month=document.getElementById('bd-month')?.value; if(!month) return; // Show auto-breakdown from monthly data let totSav=0,totMem=0,totBday=0,totSoc=0,totPen=0; DB.members.forEach(m=>{ const rows=DB.monthlyData[m.name]||[]; const row=rows.find(r=>r.m===month); if(row){totSav+=row.sav||0;totMem+=row.mem||0;totBday+=row.bday||0;totSoc+=row.soc||0;totPen+=row.pen||0;} }); const total=totSav+totMem+totBday+totSoc+totPen; const el=document.getElementById('bd-auto-breakdown'); if(el) el.innerHTML=`
Savings:
${fmtK(totSav)}
Membership:
${fmtK(totMem)}
Birthday Fund:
${fmtK(totBday)}
Social Fund:
${fmtK(totSoc)}
Penalty:
${fmtK(totPen)}
TOTAL:
${fmtK(total)}
`; } function autoFillBD(){ const month=document.getElementById('bd-month')?.value; if(!month) return; let totSav=0,totMem=0,totBday=0,totSoc=0,totPen=0; DB.members.forEach(m=>{ const rows=DB.monthlyData[m.name]||[]; const row=rows.find(r=>r.m===month); if(row){totSav+=row.sav||0;totMem+=row.mem||0;totBday+=row.bday||0;totSoc+=row.soc||0;totPen+=row.pen||0;} }); document.getElementById('bd-sav').value=totSav.toFixed(2); document.getElementById('bd-mem').value=totMem.toFixed(2); document.getElementById('bd-bday').value=totBday.toFixed(2); document.getElementById('bd-soc').value=totSoc.toFixed(2); document.getElementById('bd-pen').value=totPen.toFixed(2); document.getElementById('bd-other').value='0'; calcBDTotal(); } function calcBDTotal(){ const s=parseFloat(document.getElementById('bd-sav')?.value)||0; const m=parseFloat(document.getElementById('bd-mem')?.value)||0; const b=parseFloat(document.getElementById('bd-bday')?.value)||0; const sc=parseFloat(document.getElementById('bd-soc')?.value)||0; const p=parseFloat(document.getElementById('bd-pen')?.value)||0; const o=parseFloat(document.getElementById('bd-other')?.value)||0; const tot=s+m+b+sc+p+o; const el=document.getElementById('bd-total'); if(el) el.value=fmtK(tot); } function postBankDeposit(){ const month=document.getElementById('bd-month')?.value; const date=document.getElementById('bd-date')?.value; const s=parseFloat(document.getElementById('bd-sav')?.value)||0; const m=parseFloat(document.getElementById('bd-mem')?.value)||0; const b=parseFloat(document.getElementById('bd-bday')?.value)||0; const sc=parseFloat(document.getElementById('bd-soc')?.value)||0; const p=parseFloat(document.getElementById('bd-pen')?.value)||0; const o=parseFloat(document.getElementById('bd-other')?.value)||0; const total=s+m+b+sc+p+o; const ref=document.getElementById('bd-ref')?.value||''; const bank=document.getElementById('bd-bank')?.value||''; const notes=document.getElementById('bd-notes')?.value||''; if(!date){alert('Please enter the date banked.');return;} if(!total){alert('Total cannot be zero.');return;} const rec={id:uid(),month,date,totalDeposited:total,breakdown:{savings:s,membership:m,birthday:b,social:sc,penalty:p,other:o},bankRef:ref,bankName:bank,notes,slip:window._bdSlip||null,verifiedBy:CU?CU.name:'Treasurer',ts:new Date().toISOString()}; DB.bankDeposits.push(rec); const ok=document.getElementById('bd-ok'); if(ok){ok.style.display='flex';setTimeout(()=>ok.style.display='none',4000);} window._bdSlip=null; ['bd-date','bd-sav','bd-mem','bd-bday','bd-soc','bd-pen','bd-other','bd-ref','bd-bank','bd-notes'].forEach(id=>{const el=document.getElementById(id);if(el)el.value='';}); document.getElementById('bd-total').value=''; const fp=document.getElementById('bd-file-preview');if(fp)fp.innerHTML='📎 Click to attach bank deposit slip'; const ff=document.getElementById('bd-file');if(ff)ff.value=''; renderBankDeposits(); if(_db) _fbAdd(_COLS.bankDeposits,rec).catch(console.warn); } function renderBankDeposits(){ const tbody=document.getElementById('bd-tbody'); if(!tbody) return; const sorted=[...DB.bankDeposits].sort((a,b)=>(b.ts||'').localeCompare(a.ts||'')); const MONTHS=['Aug 25','Sep 25','Oct 25','Nov 25','Dec 25','Jan 26','Feb 26','Mar 26','Apr 26','May 26','Jun 26','Jul 26']; // Monthly table if(!sorted.length){tbody.innerHTML='
No bank deposits recorded yet
'; } else { tbody.innerHTML=sorted.map(bd=>` ${bd.month} ${fmtK(bd.totalDeposited)} ${fmtK(bd.breakdown?.savings||0)} sav + ${fmtK((bd.breakdown?.membership||0)+(bd.breakdown?.birthday||0)+(bd.breakdown?.social||0))} funds ${bd.bankRef||'—'} ${bd.bankName?'· '+bd.bankName:''} ${bd.date} ${bd.slip?``:'—'} `).join(''); } // Unbanked months warning const bankedMonths=new Set(DB.bankDeposits.map(b=>b.month)); const activityMonths=new Set(); DB.members.forEach(m=>{(DB.monthlyData[m.name]||[]).forEach(r=>{if((r.sav||0)+(r.mem||0)+(r.bday||0)+(r.soc||0)>0)activityMonths.add(r.m);});}); const unbanked=[...activityMonths].filter(m=>!bankedMonths.has(m)).sort((a,b)=>MONTHS.indexOf(a)-MONTHS.indexOf(b)); const ub=document.getElementById('bd-unbanked'); if(ub){ ub.innerHTML=!unbanked.length?'
✅ All months with deposits have been banked.
': `
${unbanked.map(m=>`
⚠️ ${m} – deposits collected but not yet banked
`).join('')}
`; } // Annual summary const asTot=DB.bankDeposits.reduce((a,b)=>a+b.totalDeposited,0); const as=document.getElementById('bd-annual-summary'); if(as) as.innerHTML=!DB.bankDeposits.length?'
No data
':`
TOTAL BANKED
${fmtK(asTot)}
DEPOSITS MADE
${DB.bankDeposits.length}
`; } // ═══════════════════════════════════════════════════════════ // COMPOUND INTEREST // ═══════════════════════════════════════════════════════════ function updateCIBadge(){ const m=document.getElementById('ci-month')?.value; const b=document.getElementById('ci-month-badge'); if(b&&m) b.textContent=m; } function loadCIInfo(){ const name=document.getElementById('ci-member')?.value; if(!name){document.getElementById('ci-info-content').textContent='Select a member.';return;} const m=gm(name); if(!m) return; const month=document.getElementById('ci-month')?.value; const rows=DB.monthlyData[name]||[]; const row=rows.find(r=>r.m===month); const alreadyPosted=DB.compoundInterest.filter(c=>c.member===name&&c.month===month).reduce((a,c)=>a+c.amount,0); document.getElementById('ci-info-content').innerHTML=` ${rrow('Cumulative Savings',fmtK(m.savings))} ${rrow('Total Interest Earned',fmtK(m.interest))} ${rrow('Outstanding Loan',fmtK(m.outstanding))} ${rrow('Already Posted This Month',alreadyPosted>0?''+fmtK(alreadyPosted)+'':'—')}`; // Pre-fill rate from formula const rateEl=document.getElementById('ci-rate'); if(rateEl&&!rateEl.value) rateEl.value=DB.formula.savingsRate||5; const periEl=document.getElementById('ci-rate-period'); if(periEl&&!periEl.value) periEl.value=DB.formula.savingsRatePeriod||'annum'; calcCI(); renderCIAllTable(); } function calcCI(){ const name=document.getElementById('ci-member')?.value; if(!name) return; const m=gm(name); if(!m) return; const rate=parseFloat(document.getElementById('ci-rate')?.value)||0; const period=document.getElementById('ci-rate-period')?.value||'annum'; const basis=document.getElementById('ci-basis')?.value||'cumulative'; const customBase=parseFloat(document.getElementById('ci-custom-base')?.value)||0; const monthlyRate=period==='annum'?rate/12:rate; let base=basis==='cumulative'?m.savings:basis==='opening'?m.savings:customBase; const interest=base*(monthlyRate/100); const res=document.getElementById('ci-calc-result'); if(res) res.innerHTML=`${fmtK(base)} × ${monthlyRate.toFixed(4)}%/month = ${fmtK(interest)}`; // Auto-fill amount const amtEl=document.getElementById('ci-amount'); if(amtEl) amtEl.value=interest.toFixed(2); } function postCompoundInterest(nameOverride,amountOverride){ const name=nameOverride||document.getElementById('ci-member')?.value; const month=document.getElementById('ci-month')?.value; const amount=amountOverride!=null?amountOverride:(parseFloat(document.getElementById('ci-amount')?.value)||0); const note=document.getElementById('ci-note')?.value||('Compound interest — '+month); const rate=parseFloat(document.getElementById('ci-rate')?.value)||DB.formula.savingsRate; const basis=document.getElementById('ci-basis')?.value||'cumulative'; if(!name){alert('Select a member.');return;} if(!amount){alert('Amount is zero.');return;} const m=gm(name); if(!m) return; // Update member interest total + monthly data m.interest+=amount; if(!DB.monthlyData[name]) DB.monthlyData[name]=[]; const rows=DB.monthlyData[name]; let row=rows.find(r=>r.m===month); if(row){row.int+=amount;row.totInt+=amount;} else{ const MONTHS=['Aug 25','Sep 25','Oct 25','Nov 25','Dec 25','Jan 26','Feb 26','Mar 26','Apr 26','May 26','Jun 26','Jul 26']; const idx=MONTHS.indexOf(month);const pr=idx>0?rows.find(r=>r.m===MONTHS[idx-1]):null; rows.push({m:month,mem:0,bday:0,soc:0,pen:0,sav:0,int:amount,cumSav:pr?pr.cumSav:m.savings,dis:0,rep:0,intPay:0,out:m.outstanding,totInt:(pr?pr.totInt:m.interest)}); rows.sort((a,b)=>MONTHS.indexOf(a.m)-MONTHS.indexOf(b.m)); } const entry={id:uid(),member:name,month,amount,rate,basis,note,postedBy:CU?CU.name:'Treasurer',ts:new Date().toISOString()}; DB.compoundInterest.push(entry); const rec={id:uid(),type:'COMPOUND INTEREST',member:name,date:tday(),amtStr:fmtK(amount), rows:[{k:'Member',v:name},{k:'Month',v:month},{k:'Amount',v:fmtK(amount)},{k:'Rate',v:rate+'%'},{k:'Basis',v:basis},{k:'Note',v:note}], total:fmtK(amount),totalLabel:'INTEREST POSTED'}; DB.approved.unshift(rec); // Save to Firebase const mDoc=DB.members.find(x=>x.name===name); if(_db){_fbAdd(_COLS.compoundInt,entry).catch(console.warn);_fbAdd(_COLS.approved,rec).catch(console.warn);if(mDoc?.id)_fbSet(_COLS.members,mDoc.id,{interest:m.interest}).catch(console.warn);_fbSet(_COLS.monthly,name,{rows}).catch(console.warn);} renderCIAllTable(); renderCIHistory(); renderDashboard(); renderMembersDir(); if(!nameOverride){ const ok=document.getElementById('ci-ok'); if(ok){ok.style.display='flex';setTimeout(()=>ok.style.display='none',5000);} showReceiptPage({...rec,id:rec.id}); } } function postCIAllMembers(){ const month=document.getElementById('ci-month')?.value||'Jul 26'; const rate=parseFloat(document.getElementById('ci-rate')?.value)||DB.formula.savingsRate; const period=document.getElementById('ci-rate-period')?.value||DB.formula.savingsRatePeriod; if(!confirm('Post compound interest for ALL members for '+month+'?')) return; const mr=period==='annum'?rate/12:rate; let count=0,total=0; DB.members.forEach(m=>{const a=m.savings*(mr/100);if(a>0){postCompoundInterest(m.name,a);count++;total+=a;}}); alert('✅ Compound interest posted for '+count+' members. Total: '+fmtK(total)); } function clearCIForm(){ const sel=document.getElementById('ci-member'); if(sel) sel.selectedIndex=0; ['ci-rate','ci-custom-base','ci-amount','ci-note'].forEach(id=>{const el=document.getElementById(id);if(el)el.value='';}); const res=document.getElementById('ci-calc-result'); if(res) res.textContent='Fill in details above.'; document.getElementById('ci-info-content').textContent='Select a member to load details.'; } function renderCIAllTable(){ const month=document.getElementById('ci-month')?.value||'Jul 26'; const rate=parseFloat(document.getElementById('ci-rate')?.value)||DB.formula.savingsRate; const period=document.getElementById('ci-rate-period')?.value||DB.formula.savingsRatePeriod; const mr=period==='annum'?rate/12:rate; const tbody=document.getElementById('ci-all-tbody'); if(!tbody) return; tbody.innerHTML=DB.members.map(m=>{ const already=DB.compoundInterest.filter(c=>c.member===m.name&&c.month===month).reduce((a,c)=>a+c.amount,0); const calc=m.savings*(mr/100); return`${m.name}${fmtK(m.savings)} ${already>0?fmtK(already):'—'} ${fmtK(calc)} `; }).join(''); } function quickPostCI(name,amount){ const mo=document.getElementById('ci-month')?.value||'Jul 26'; const me=document.getElementById('ci-member'); if(me) me.value=name; loadCIInfo(); setTimeout(()=>postCompoundInterest(name,amount),50); } function renderCIHistory(){ const el=document.getElementById('ci-history'); if(!el) return; const entries=[...DB.compoundInterest].sort((a,b)=>(b.ts||'').localeCompare(a.ts||'')).slice(0,12); if(!entries.length){el.innerHTML='
No entries yet
';return;} el.innerHTML=entries.map(e=>`
${e.member}
${e.month} · ${e.rate}% · ${fmtK(e.amount)}
${e.basis}
`).join(''); } // ═══════════════════════════════════════════════════════════ // REVERSALS // ═══════════════════════════════════════════════════════════ let _revSelectedTx = null; function populateRevFilter(){ const sel=document.getElementById('rev-filter-member'); if(!sel) return; if(sel.options.length<=1) DB.members.forEach(m=>sel.add(new Option(m.name,m.name))); } function renderReversals(){ const filterMember=document.getElementById('rev-filter-member')?.value||''; const filterType=document.getElementById('rev-filter-type')?.value||''; let txs=[...DB.approved].filter(r=>!DB.reversals.some(rv=>rv.originalTxId===r.id)); if(filterMember) txs=txs.filter(r=>r.member===filterMember); if(filterType) txs=txs.filter(r=>r.type===filterType); txs=txs.slice(0,30); const el=document.getElementById('rev-tx-list'); if(!el) return; if(!txs.length){el.innerHTML='
↩️
No transactions found
';return;} el.innerHTML=txs.map(r=>`
${r.member} – ${r.type}
${r.date} · ${r.amtStr||'—'}
${r.rows?'
'+(r.rows[1]?.v||'')+'
':''}
`).join(''); // Reversal history const hist=document.getElementById('rev-history'); if(!hist) return; if(!DB.reversals.length){hist.innerHTML='
No reversals yet
';return;} hist.innerHTML=[...DB.reversals].sort((a,b)=>(b.ts||'').localeCompare(a.ts||'')).map(rv=>`
${rv.member}
REVERSED
${rv.date} · ${rv.reversedBy}
"${rv.reason}"
`).join(''); } function selectTxForReversal(id){ const tx=DB.approved.find(r=>r.id===id); if(!tx) return; _revSelectedTx=tx; const card=document.getElementById('rev-detail-card'); if(card) card.style.display=''; const prev=document.getElementById('rev-tx-preview'); if(prev) prev.innerHTML=`
Transaction Selected:
${rrow('Member',tx.member)}${rrow('Type',''+tx.type+'')} ${rrow('Amount',tx.amtStr||'—')}${rrow('Date',tx.date)} ${(tx.rows||[]).slice(0,3).map(r=>rrow(r.k,r.v)).join('')}`; const rr=document.getElementById('rev-reason'); if(rr) rr.value=''; card.scrollIntoView({behavior:'smooth'}); } function cancelReversal(){ _revSelectedTx=null; const card=document.getElementById('rev-detail-card'); if(card) card.style.display='none'; } function executeReversal(){ const tx=_revSelectedTx; if(!tx){alert('No transaction selected.');return;} const reason=document.getElementById('rev-reason')?.value?.trim(); if(!reason){alert('A reason is required — this message will be sent to the member.');return;} if(!confirm('Reverse this transaction and notify '+tx.member+'?')) return; // Adjust member balance based on transaction type const m=gm(tx.member); if(m){ if(tx.type==='DEPOSIT RECEIPT'){ const savRow=tx.rows?.find(r=>r.k==='Savings'); const savAmt=savRow?parseFloat(savRow.v.replace(/[^0-9.]/g,''))||0:0; m.savings=Math.max(0,m.savings-savAmt); const mDoc=DB.members.find(x=>x.name===m.name); if(_db&&mDoc?.id) _fbSet(_COLS.members,mDoc.id,{savings:m.savings}).catch(console.warn); } else if(tx.type==='REPAYMENT RECEIPT'){ const repRow=tx.rows?.find(r=>r.k==='Principal Repaid'); const repAmt=repRow?parseFloat(repRow.v.replace(/[^0-9.]/g,''))||0:0; m.outstanding+=repAmt; m.repayments=Math.max(0,m.repayments-repAmt); const mDoc=DB.members.find(x=>x.name===m.name); if(_db&&mDoc?.id) _fbSet(_COLS.members,mDoc.id,{outstanding:m.outstanding,repayments:m.repayments}).catch(console.warn); } else if(tx.type==='LOAN DISBURSEMENT'){ const amtRow=tx.rows?.find(r=>r.k==='Loan Amount'); const loanAmt=amtRow?parseFloat(amtRow.v.replace(/[^0-9.]/g,''))||0:0; m.outstanding=Math.max(0,m.outstanding-loanAmt); m.totalLoans=Math.max(0,m.totalLoans-loanAmt); const mDoc=DB.members.find(x=>x.name===m.name); if(_db&&mDoc?.id) _fbSet(_COLS.members,mDoc.id,{outstanding:m.outstanding,totalLoans:m.totalLoans}).catch(console.warn); } else if(tx.type==='INTEREST POSTING'||tx.type==='COMPOUND INTEREST'){ const intAmt=parseFloat((tx.amtStr||'').replace(/[^0-9.]/g,''))||0; m.interest=Math.max(0,m.interest-intAmt); const mDoc=DB.members.find(x=>x.name===m.name); if(_db&&mDoc?.id) _fbSet(_COLS.members,mDoc.id,{interest:m.interest}).catch(console.warn); } } // Create reversal record const rev={id:uid(),originalTxId:tx.id,member:tx.member,type:tx.type,originalAmount:tx.amtStr,reason,reversedBy:CU?CU.name:'Treasurer',date:tday(),ts:new Date().toISOString()}; DB.reversals.push(rev); // Send notification to the affected member sendNotification(tx.member,'reversal', '↩️ Transaction Reversed', `Your ${tx.type} of ${tx.amtStr} on ${tx.date} has been reversed by the Treasurer. Reason: "${reason}". Please contact the Treasurer if you have questions.` ); if(_db) _fbAdd(_COLS.reversals,rev).catch(console.warn); cancelReversal(); renderReversals(); renderDashboard(); renderMembersDir(); alert('✅ Transaction reversed. '+tx.member+' has been notified.'); } // ═══════════════════════════════════════════════════════════ // NOTIFICATIONS // ═══════════════════════════════════════════════════════════ function sendNotification(member, type, title, message){ const n={id:uid(),member,type,title,message,read:false,date:tday(),ts:new Date().toISOString()}; DB.notifications.push(n); updateNotifBadge(); if(_db) _fbAdd(_COLS.notifications,n).catch(console.warn); } function updateNotifBadge(){ const badge=document.getElementById('notif-badge'); if(!badge) return; const unread=DB.notifications.filter(n=>!n.read&&(CR==='treasurer'||n.member===CU?.name)).length; badge.textContent=unread>9?'9+':unread; badge.style.display=unread>0?'flex':'none'; } function renderNotifications(){ const el=document.getElementById('notif-list'); if(!el) return; const mine=DB.notifications.filter(n=>CR==='treasurer'||n.member===CU?.name) .sort((a,b)=>(b.ts||'').localeCompare(a.ts||'')); if(!mine.length){el.innerHTML='
🔔
No notifications
';return;} el.innerHTML=mine.map(n=>`
${n.type==='reversal'?'↩️':n.type==='approval'?'✅':n.type==='rejection'?'❌':'🔔'}
${n.title}
${n.message}
${n.date}${n.member?' · '+n.member:''}
${!n.read?'New':''}
`).join(''); } function markRead(id){ const n=DB.notifications.find(x=>x.id===id); if(n){n.read=true;} updateNotifBadge(); renderNotifications(); } function markAllRead(){ DB.notifications.filter(n=>CR==='treasurer'||n.member===CU?.name).forEach(n=>n.read=true); updateNotifBadge(); renderNotifications(); } // ═══════════════════════════════════════════════════════════ // SILSTATE — FINANCIAL QUOTES TICKER // ═══════════════════════════════════════════════════════════ const QUOTES = [ {q:"Do not save what is left after spending, but spend what is left after saving.",a:"Warren Buffett"}, {q:"A budget is telling your money where to go instead of wondering where it went.",a:"Dave Ramsey"}, {q:"The habit of saving is itself an education; it fosters every virtue and teaches self-denial.",a:"T.T. Munger"}, {q:"Financial peace isn't the acquisition of stuff. It's learning to live on less than you make.",a:"Dave Ramsey"}, {q:"It's not how much money you make, but how much money you keep, how hard it works for you.",a:"Robert Kiyosaki"}, {q:"Wealth consists not in having great possessions, but in having few wants.",a:"Epictetus"}, {q:"An investment in knowledge pays the best interest.",a:"Benjamin Franklin"}, {q:"Never spend your money before you have earned it.",a:"Thomas Jefferson"}, {q:"Beware of little expenses; a small leak will sink a great ship.",a:"Benjamin Franklin"}, {q:"Money is a terrible master but an excellent servant.",a:"P.T. Barnum"}, {q:"A group that saves together, grows together.",a:"African Proverb"}, {q:"Small amounts saved daily add up to mighty sums.",a:"Silstate Wisdom"}, {q:"Accountability in group savings builds community wealth.",a:"Silstate Business Solutions"}, {q:"The discipline to save consistently is worth more than a windfall.",a:"John Templeton"}, {q:"Compound interest is the eighth wonder of the world. He who understands it, earns it.",a:"Albert Einstein"}, {q:"Financial freedom is available to those who learn about it and work for it.",a:"Robert Kiyosaki"}, {q:"Investing in your savings group is investing in your community.",a:"Silstate Business Solutions"}, {q:"The goal is not just to save money — it is to build a future together.",a:"Silstate Wisdom"}, {q:"Transparency and trust are the pillars of every successful savings group.",a:"Silstate Business Solutions"}, {q:"Good record-keeping today prevents financial disputes tomorrow.",a:"Silstate Wisdom"}, {q:"Every contribution, no matter how small, moves the group forward.",a:"African Savings Proverb"}, {q:"Save today so your tomorrow can be the day you dreamed of.",a:"Silstate Wisdom"}, {q:"The secret of getting ahead is getting started — one deposit at a time.",a:"Mark Twain (adapted)"}, {q:"Savings groups are the original social network — community wealth in action.",a:"Silstate Business Solutions"}, ]; let _quoteIdx = Math.floor(Math.random()*QUOTES.length); function rotateQuote(){ const q=QUOTES[_quoteIdx%QUOTES.length]; const el=document.getElementById('quote-text'); if(el) el.textContent='"'+q.q+'" — '+q.a; _quoteIdx++; } rotateQuote(); setInterval(rotateQuote,14000); // ═══════════════════════════════════════════════════════════ // MULTI-GROUP MANAGEMENT // ═══════════════════════════════════════════════════════════ const GRP_KEY='silstate_groups'; let _groups=[]; let _activeGroupId=null; function loadGroupsFromStorage(){ try{const s=localStorage.getItem(GRP_KEY);if(s)_groups=JSON.parse(s);}catch(e){_groups=[];} if(!_groups.find(g=>g.id==='default')){ _groups.push({id:'default',name:'Side Savers',year:'Aug 2025 – Jul 2026',treasurerPin:'admin123',logoUrl:null,firebaseConfig:null,createdAt:new Date().toISOString()}); saveGroupsToStorage(); } } function saveGroupsToStorage(){ localStorage.setItem(GRP_KEY,JSON.stringify(_groups)); } function getActiveGroup(){ return _groups.find(g=>g.id===_activeGroupId)||_groups[0]||null; } function showGroupSelectScreen(){ loadGroupsFromStorage(); document.getElementById('group-select-screen').style.display='flex'; document.getElementById('login-screen').style.display='none'; document.getElementById('app').style.display='none'; const list=document.getElementById('group-list-items'); if(!list) return; if(!_groups.length){list.innerHTML='
No groups yet
';return;} list.innerHTML=_groups.map(g=>`
${g.name}
${g.year||'—'} · ${g.firebaseConfig?'☁️ Cloud Sync':'💾 Local'}
${_groups.length>1?``:''}
`).join(''); } function selectGroup(id){ _activeGroupId=id; localStorage.setItem('silstate_active_group',id); const g=getActiveGroup(); if(!g) return; DB.groupName=g.name||'Savings Group'; DB.groupYear=g.year||''; DB.treasurerPin=g.treasurerPin||'admin123'; DB.logoUrl=g.logoUrl||null; if(g.firebaseConfig) localStorage.setItem('ss_firebase_config',JSON.stringify(g.firebaseConfig)); else localStorage.removeItem('ss_firebase_config'); document.getElementById('group-select-screen').style.display='none'; buildLoginSelect(); applySettings(); document.getElementById('login-screen').style.display='flex'; } function switchGroup(){ CU=null; CR='member'; document.getElementById('app').style.display='none'; document.getElementById('l-who').value=''; document.getElementById('l-pin').value=''; showGroupSelectScreen(); } function openAddGroupModal(){ openModal('modal-add-group'); ['ag-name','ag-year','ag-pin','ag-apikey','ag-projectid','ag-authdomain','ag-storagebucket','ag-senderid','ag-appid'].forEach(id=>{const el=document.getElementById(id);if(el)el.value='';}); document.getElementById('ag-year').value='Aug 2025 – Jul 2026'; document.getElementById('ag-pin').value='admin123'; document.getElementById('ag-fb-section').style.display='none'; document.getElementById('ag-use-fb').checked=false; } function toggleAGFirebase(){ const use=document.getElementById('ag-use-fb')?.checked; document.getElementById('ag-fb-section').style.display=use?'':'none'; } function saveNewGroup(){ const name=(document.getElementById('ag-name')?.value||'').trim(); const year=document.getElementById('ag-year')?.value||''; const pin=document.getElementById('ag-pin')?.value||'admin123'; if(!name){alert('Group name is required.');return;} const useFB=document.getElementById('ag-use-fb')?.checked; let fbCfg=null; if(useFB){ const apiKey=document.getElementById('ag-apikey')?.value?.trim(); const projectId=document.getElementById('ag-projectid')?.value?.trim(); if(!apiKey||!projectId){alert('API Key and Project ID are required for Firebase.');return;} fbCfg={apiKey,projectId, authDomain:document.getElementById('ag-authdomain')?.value?.trim(), storageBucket:document.getElementById('ag-storagebucket')?.value?.trim(), messagingSenderId:document.getElementById('ag-senderid')?.value?.trim(), appId:document.getElementById('ag-appid')?.value?.trim()}; } const newGroup={id:'grp_'+Date.now(),name,year,treasurerPin:pin,logoUrl:null,firebaseConfig:fbCfg,createdAt:new Date().toISOString()}; _groups.push(newGroup); saveGroupsToStorage(); closeModal('modal-add-group'); showGroupSelectScreen(); } function editGroupConfig(id){ const g=_groups.find(x=>x.id===id); if(!g) return; openModal('modal-add-group'); document.getElementById('ag-name').value=g.name; document.getElementById('ag-year').value=g.year||''; document.getElementById('ag-pin').value=g.treasurerPin||'admin123'; if(g.firebaseConfig){ document.getElementById('ag-use-fb').checked=true; document.getElementById('ag-fb-section').style.display=''; document.getElementById('ag-apikey').value=g.firebaseConfig.apiKey||''; document.getElementById('ag-projectid').value=g.firebaseConfig.projectId||''; document.getElementById('ag-authdomain').value=g.firebaseConfig.authDomain||''; document.getElementById('ag-storagebucket').value=g.firebaseConfig.storageBucket||''; document.getElementById('ag-senderid').value=g.firebaseConfig.messagingSenderId||''; document.getElementById('ag-appid').value=g.firebaseConfig.appId||''; } // Override save button to edit mode const saveBtn=document.querySelector('#modal-add-group .mf .btn.bp'); if(saveBtn) saveBtn.setAttribute('onclick',`saveEditGroup('${id}')`); } function saveEditGroup(id){ const g=_groups.find(x=>x.id===id); if(!g) return; g.name=document.getElementById('ag-name')?.value||g.name; g.year=document.getElementById('ag-year')?.value||g.year; g.treasurerPin=document.getElementById('ag-pin')?.value||g.treasurerPin; const useFB=document.getElementById('ag-use-fb')?.checked; g.firebaseConfig=useFB?{apiKey:document.getElementById('ag-apikey')?.value,projectId:document.getElementById('ag-projectid')?.value,authDomain:document.getElementById('ag-authdomain')?.value,storageBucket:document.getElementById('ag-storagebucket')?.value,messagingSenderId:document.getElementById('ag-senderid')?.value,appId:document.getElementById('ag-appid')?.value}:null; saveGroupsToStorage(); if(id===_activeGroupId){DB.groupName=g.name;DB.groupYear=g.year;DB.treasurerPin=g.treasurerPin;applySettings();} closeModal('modal-add-group'); showGroupSelectScreen(); } function deleteGroup(id){ if(_groups.length<=1){alert('Cannot delete the only group.');return;} const g=_groups.find(x=>x.id===id); if(!confirm('Delete "'+g?.name+'"? Cloud data (if any) remains in Firebase.')) return; _groups=_groups.filter(x=>x.id!==id); saveGroupsToStorage(); if(_activeGroupId===id){_activeGroupId=null;} showGroupSelectScreen(); } // ═══════════════════════════════════════════════════════════ // COMBINED TRANSACTIONS TAB (Deposit + Repayment) // ═══════════════════════════════════════════════════════════ let _activeTxTab='deposit'; function switchTxTab(tab){ _activeTxTab=tab; ['deposit','repayment'].forEach(t=>{ const el=document.getElementById('txp-'+t); if(el) el.style.display=t===tab?'':'none'; const bt=document.getElementById('txtab-'+t); if(bt) bt.classList.toggle('active',t===tab); }); if(tab==='repayment'){loadRepayInfo();renderOutstandingTable();} } // ═══════════════════════════════════════════════════════════ // FORMULA QUICK-EDIT MODAL (accessible from disburse, etc.) // ═══════════════════════════════════════════════════════════ function showFormulaQuickEdit(){ const f=DB.formula; if(!f) return; openModal('modal-formula-quick'); const sv=(id,v)=>{const el=document.getElementById(id);if(el)el.value=v;}; sv('fq-loan-rate',f.loanRate); sv('fq-loan-period',f.loanRatePeriod); sv('fq-loan-method',f.loanMethod); sv('fq-sav-rate',f.savingsRate); sv('fq-sav-period',f.savingsRatePeriod); sv('fq-sav-method',f.savingsMethod); sv('fq-elig',f.eligibilityMultiplier); sv('fq-pen-rate',f.penaltyRate); sv('fq-pen-flat',f.lateFeeFlat); } function saveFormulaQuick(){ const f=DB.formula; const gv=id=>{const el=document.getElementById(id);return el?el.value:null;}; const gn=id=>parseFloat(document.getElementById(id)?.value)||0; f.loanRate=gn('fq-loan-rate'); f.loanRatePeriod=gv('fq-loan-period'); f.loanMethod=gv('fq-loan-method'); f.savingsRate=gn('fq-sav-rate'); f.savingsRatePeriod=gv('fq-sav-period'); f.savingsMethod=gv('fq-sav-method'); f.eligibilityMultiplier=gn('fq-elig'); f.penaltyRate=gn('fq-pen-rate'); f.lateFeeFlat=gn('fq-pen-flat'); DB.interestRate=f.loanRate; f.auditLog.unshift({date:tday(),time:new Date().toLocaleTimeString(),field:'Quick Edit',old:'—',new:'Updated via Quick Edit',changedBy:CU?CU.name:'Treasurer'}); if(_db&&_COLS){_fbSet(_COLS.formula,'config',f).catch(console.warn);_fbSet(_COLS.settings,'config',{interestRate:DB.interestRate}).catch(console.warn);} closeModal('modal-formula-quick'); renderInterestSchedule(); renderDashboard(); alert('✅ Formula settings updated!'); } // ═══════════════════════════════════════════════════════════ // FIREBASE INTEGRATION // ═══════════════════════════════════════════════════════════ const _FB_KEY = 'ss_firebase_config'; let _db = null; const _COLS = { settings:'ss_settings', members:'ss_members', monthly:'ss_monthly', pending:'ss_pending', approved:'ss_approved', formula:'ss_formula', expenses:'ss_expenses', bankDeposits:'ss_bankdeposits', compoundInt:'ss_compoundint', reversals:'ss_reversals', notifications:'ss_notifications' }; let _unsubs = []; function _setSync(s){ const d=document.getElementById('sync-dot'); if(!d) return; d.className='sync-dot'+(s==='syncing'?' syncing':s==='offline'?' offline':''); d.title=s==='syncing'?'Saving…':s==='offline'?'Offline':'Live sync'; } // Try to connect Firebase on page load (if config exists) (async function tryAutoConnect(){ try{ const _cfg={ apiKey:"AIzaSyAKc0pAwJU9IuDvTX4rDBrNROo1SBIaEns", authDomain:"side-savers.firebaseapp.com", projectId:"side-savers", storageBucket:"side-savers.firebasestorage.app", messagingSenderId:"694912552456", appId:"1:694912552456:web:0551d1c0695a11cd180bbd" }; if(!firebase.apps.length) firebase.initializeApp(_cfg); _db=firebase.firestore(); await _db.enablePersistence({synchronizeTabs:true}).catch(()=>{}); console.log('[Silstate] Firebase connected'); _setSync('ok'); await _fbLoad(); _fbListen(); // Show login with Firebase data const s=document.getElementById('fb-status');if(s)s.textContent=DB.members.length+' members loaded'; setTimeout(()=>{ const l=document.getElementById('fb-loader');if(l)l.remove(); _buildLoginSelect(); document.getElementById('login-screen').style.display='flex'; },400); }catch(e){ console.warn('[Silstate] Firebase unavailable — running offline',e); _setSync('offline'); const l=document.getElementById('fb-loader');if(l)l.remove(); _buildLoginSelect(); document.getElementById('login-screen').style.display='flex'; } })(); function _buildLoginSelect(){ const sel=document.getElementById('l-who'); if(!sel) return; sel.innerHTML=''; DB.members.forEach(m=>{ const o=document.createElement('option'); o.value=m.name; o.textContent=m.name; sel.appendChild(o); }); const t=document.createElement('option'); t.value='__treasurer__'; t.textContent='🔐 Treasurer / Admin'; sel.appendChild(t); applySettings(); } window.addEventListener('online', ()=>{ if(_db) _setSync('ok'); document.getElementById('offline-bar').style.display='none'; }); window.addEventListener('offline', ()=>{ _setSync('offline'); document.getElementById('offline-bar').style.display=''; }); // ── LOAD all data from Firestore ────────────────────────── async function _fbLoad(){ if(!_db) return; _setSync('syncing'); try{ // Settings const sd=await _db.collection(_COLS.settings).doc('config').get(); if(sd.exists){ const s=sd.data(); DB.groupName=s.groupName||DB.groupName; DB.groupYear=s.groupYear||DB.groupYear; DB.interestRate=s.interestRate||DB.interestRate; DB.treasurerPin=s.treasurerPin||DB.treasurerPin; DB.logoUrl=s.logoUrl||null; } else { // Seed settings on first run await _db.collection(_COLS.settings).doc('config').set({ groupName:DB.groupName,groupYear:DB.groupYear, interestRate:DB.interestRate,treasurerPin:DB.treasurerPin,logoUrl:null }); } // Formula const fd=await _db.collection(_COLS.formula).doc('config').get(); if(fd.exists) DB.formula={...DB.formula,...fd.data()}; // Members const ms=await _db.collection(_COLS.members).orderBy('order').get(); if(!ms.empty){ DB.members=ms.docs.map(d=>({id:d.id,...d.data()})); } else { // Seed members const batch=_db.batch(); DB.members.forEach((m,i)=>{ const ref=_db.collection(_COLS.members).doc(); batch.set(ref,{...m,order:i}); m.id=ref.id; }); await batch.commit(); } // Monthly data const mds=await _db.collection(_COLS.monthly).get(); mds.forEach(d=>{DB.monthlyData[d.id]=d.data().rows||[];}); // Pending const ps=await _db.collection(_COLS.pending).where('status','==','pending').get(); DB.pending={deposits:[],repayments:[]}; ps.forEach(d=>{const item={id:d.id,...d.data()};if(item.type==='deposit')DB.pending.deposits.push(item);else DB.pending.repayments.push(item);}); // Approved (latest 100) const as=await _db.collection(_COLS.approved).orderBy('ts','desc').limit(100).get(); DB.approved=as.docs.map(d=>({id:d.id,...d.data()})); // Reversals try{ const rvs=await _db.collection(_COLS.reversals).get(); DB.reversals=rvs.docs.map(d=>({id:d.id,...d.data()})); }catch(e){} // Notifications try{ const ns=await _db.collection(_COLS.notifications).get(); DB.notifications=ns.docs.map(d=>({id:d.id,...d.data()})); }catch(e){} _setSync('ok'); }catch(e){ console.error('[FB load]',e); _setSync('offline'); } } // ── Real-time listeners ──────────────────────────────────── function _fbListen(){ if(!_db) return; _unsubs.forEach(u=>u()); _unsubs=[]; _unsubs.push(_db.collection(_COLS.members).orderBy('order').onSnapshot(snap=>{ if(!snap.metadata.hasPendingWrites){ DB.members=snap.docs.map(d=>({id:d.id,...d.data()})); renderDashboard(); renderMembersDir(); populateSels(); renderInterestSchedule(); } _setSync('ok'); },()=>_setSync('offline'))); _unsubs.push(_db.collection(_COLS.pending).where('status','==','pending').onSnapshot(snap=>{ DB.pending={deposits:[],repayments:[]}; snap.forEach(d=>{const i={id:d.id,...d.data()};if(i.type==='deposit')DB.pending.deposits.push(i);else DB.pending.repayments.push(i);}); updatePendingCount(); if(document.getElementById('page-verify')?.classList.contains('active'))renderAllPending(); })); _unsubs.push(_db.collection(_COLS.approved).orderBy('ts','desc').limit(50).onSnapshot(snap=>{ DB.approved=snap.docs.map(d=>({id:d.id,...d.data()})); renderRecentRx(); })); } // ── Write helpers ────────────────────────────────────────── async function _fbSet(col,docId,data){ if(!_db)return; _setSync('syncing'); try{ await _db.collection(col).doc(docId).set(data,{merge:true}); _setSync('ok'); } catch(e){ console.warn('[FB set]',e); _setSync('offline'); } } async function _fbAdd(col,data){ if(!_db)return null; _setSync('syncing'); try{ const r=await _db.collection(col).add({...data,ts:firebase.firestore.FieldValue.serverTimestamp()}); _setSync('ok'); return r.id; } catch(e){ console.warn('[FB add]',e); _setSync('offline'); return null; } } async function _fbDel(col,id){ if(!_db)return; _setSync('syncing'); try{ await _db.collection(col).doc(id).delete(); _setSync('ok'); } catch(e){ console.warn('[FB del]',e); } } // ── Patch existing functions to also write to Firebase ───── // Wrap approveDeposit const _origApproveDeposit=approveDeposit; window.approveDeposit=async function(id){ _origApproveDeposit(id); const item=DB.pending.deposits.find(i=>i.id===id); if(item){ await _fbSet(_COLS.pending,id,{status:'approved'}); const m=gm(item.member); const mDoc=DB.members.find(x=>x.name===item.member); if(mDoc?.id) await _fbSet(_COLS.members,mDoc.id,{savings:m.savings}); const rec=DB.approved[0]; if(rec) await _fbAdd(_COLS.approved,{...rec,ts:null}); } }; const _origApproveRepay=approveRepay; window.approveRepay=async function(id){ _origApproveRepay(id); const item=DB.pending.repayments.find(i=>i.id===id); if(item){ await _fbSet(_COLS.pending,id,{status:'approved'}); const m=gm(item.member); const mDoc=DB.members.find(x=>x.name===item.member); if(mDoc?.id) await _fbSet(_COLS.members,mDoc.id,{outstanding:m.outstanding,repayments:m.repayments}); const rec=DB.approved[0]; if(rec) await _fbAdd(_COLS.approved,{...rec,ts:null}); } }; const _origSubmitDeposit=submitDeposit; window.submitDeposit=async function(){ _origSubmitDeposit(); const item=DB.pending.deposits[DB.pending.deposits.length-1]; if(item) await _fbAdd(_COLS.pending,item); }; const _origSubmitRepay=submitRepay; window.submitRepay=async function(){ _origSubmitRepay(); const item=DB.pending.repayments[DB.pending.repayments.length-1]; if(item) await _fbAdd(_COLS.pending,item); }; const _origSaveMember=saveMember; window.saveMember=async function(){ _origSaveMember(); const m=DB.members[DB.members.length-1]; if(m){ if(!m.id){const id=await _fbAdd(_COLS.members,{...m,order:DB.members.length-1});if(id)m.id=id;} else await _fbSet(_COLS.members,m.id,m); } }; const _origSaveBalance=saveBalance; window.saveBalance=async function(){ _origSaveBalance(); const i=parseInt(document.getElementById('mbal-idx')?.value); const m=DB.members[i]; if(m?.id) await _fbSet(_COLS.members,m.id,{savings:m.savings,outstanding:m.outstanding,interest:m.interest,totalLoans:m.totalLoans,repayments:m.repayments,eligibility:m.eligibility}); }; const _origChangePIN=changePIN; window.changePIN=async function(){ _origChangePIN(); if(CR==='treasurer') await _fbSet(_COLS.settings,'config',{treasurerPin:DB.treasurerPin}); else{const m=gm(CU.name);if(m?.id)await _fbSet(_COLS.members,m.id,{pin:m.pin});} }; const _origUpdateContact=updateContact; window.updateContact=async function(){ _origUpdateContact(); const m=gm(CU.name); if(m?.id) await _fbSet(_COLS.members,m.id,{phone:m.phone,occ:m.occ,bio:m.bio}); }; const _origSaveSettings=saveSettings; window.saveSettings=async function(){ _origSaveSettings(); await _fbSet(_COLS.settings,'config',{groupName:DB.groupName,groupYear:DB.groupYear,interestRate:DB.interestRate,treasurerPin:DB.treasurerPin}); }; const _origSaveFormulaSettings=saveFormulaSettings; window.saveFormulaSettings=async function(){ _origSaveFormulaSettings(); await _fbSet(_COLS.formula,'config',DB.formula); await _fbSet(_COLS.settings,'config',{interestRate:DB.interestRate}); }; const _origPostInterest=postInterest; window.postInterest=async function(nameOverride,amountOverride){ const rec=await _origPostInterest(nameOverride,amountOverride); const name=nameOverride||document.getElementById('pi-member')?.value; const m=gm(name); if(m?.id) await _fbSet(_COLS.members,m.id,{interest:m.interest}); await _fbSet(_COLS.monthly,name,{rows:DB.monthlyData[name]||[]}); if(rec) await _fbAdd(_COLS.approved,{...rec,ts:null}); return rec; }; const _origDisburseLoan=disburseLoan; window.disburseLoan=async function(){ _origDisburseLoan(); const name=document.getElementById('dis-mem')?.value; const m=gm(name); if(m?.id) await _fbSet(_COLS.members,m.id,{outstanding:m.outstanding,totalLoans:m.totalLoans}); await _fbSet(_COLS.monthly,name,{rows:DB.monthlyData[name]||[]}); const rec=DB.approved[0]; if(rec) await _fbAdd(_COLS.approved,{...rec,ts:null}); }; const _origHandleLogo=handleLogo; window.handleLogo=async function(e){ _origHandleLogo(e); setTimeout(async()=>{ if(DB.logoUrl) await _fbSet(_COLS.settings,'config',{logoUrl:DB.logoUrl}); },500); };